package com.demo.web.servlet;

import java.io.IOException;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.FrameworkServlet;

@SuppressWarnings("serial")
public class DelegatingServletProxy extends FrameworkServlet {

	private String contextAttribute;

	private WebApplicationContext webApplicationContext;

	private String targetBeanName;

	private boolean targetServletLifecycle = false;

	private volatile Servlet delegate;

	private final Object delegateMonitor = new Object();

	/**
	 * Create a new {@code DelegatingServletProxy}. For traditional (pre-Servlet 3.0) use
	 * in {@code web.xml}.
	 * @see #setTargetBeanName(String)
	 */
	public DelegatingServletProxy() {}

	/**
	 * Create a new {@code DelegatingServletProxy} with the given {@link Servlet} delegate.
	 * Bypasses entirely the need for interacting with a Spring application context,
	 * specifying the {@linkplain #setTargetBeanName target bean name}, etc.
	 * <p>For use in Servlet 3.0+ environments where instance-based registration of
	 * servlets is supported.
	 * @param delegate the {@code Servlet} instance that this proxy will delegate to and
	 * manage the lifecycle for (must not be {@code null}).
	 * @see #doService(ServletRequest, ServletResponse)
	 * @see #invokeDelegate(Servlet, ServletRequest, ServletResponse)
	 * @see #destroy()
	 * @see #setEnvironment(org.springframework.core.env.Environment)
	 */
	public DelegatingServletProxy(Servlet delegate) {
		Assert.notNull(delegate, "delegate Servlet object must not be null");
		this.delegate = delegate;
	}

	/**
	 * Create a new {@code DelegatingServletProxy} that will retrieve the named target
	 * bean from the Spring {@code WebApplicationContext} found in the {@code ServletContext}
	 * (either the 'root' application context or the context named by
	 * {@link #setContextAttribute}).
	 * <p>For use in Servlet 3.0+ environments where instance-based registration of
	 * servlets is supported.
	 * <p>The target bean must implement the standard Servlet Filter.
	 * @param targetBeanName name of the target servlet bean to look up in the Spring
	 * application context (must not be {@code null}).
	 * @see #findWebApplicationContext()
	 * @see #setEnvironment(org.springframework.core.env.Environment)
	 */
	public DelegatingServletProxy(String targetBeanName) {
		this(targetBeanName, null);
	}

	/**
	 * Create a new {@code DelegatingServletProxy} that will retrieve the named target
	 * bean from the given Spring {@code WebApplicationContext}.
	 * <p>For use in Servlet 3.0+ environments where instance-based registration of
	 * servlets is supported.
	 * <p>The target bean must implement the standard Servlet Filter interface.
	 * <p>The given {@code WebApplicationContext} may or may not be refreshed when passed
	 * in. If it has not, and if the context implements {@link ConfigurableApplicationContext},
	 * a {@link ConfigurableApplicationContext#refresh() refresh()} will be attempted before
	 * retrieving the named target bean.
	 * <p>This proxy's {@code Environment} will be inherited from the given
	 * {@code WebApplicationContext}.
	 * @param targetBeanName name of the target servlet bean in the Spring application
	 * context (must not be {@code null}).
	 * @param wac the application context from which the target servlet will be retrieved;
	 * if {@code null}, an application context will be looked up from {@code ServletContext}
	 * as a fallback.
	 * @see #findWebApplicationContext()
	 * @see #setEnvironment(org.springframework.core.env.Environment)
	 */
	public DelegatingServletProxy(String targetBeanName, WebApplicationContext wac) {
		Assert.hasText(targetBeanName, "target Servlet bean name must not be null or empty");
		this.setTargetBeanName(targetBeanName);
		this.webApplicationContext = wac;
		if (wac != null) {
			this.setEnvironment(wac.getEnvironment());
		}
	}

	/**
	 * Set the name of the ServletContext attribute which should be used to retrieve the
	 * {@link WebApplicationContext} from which to load the delegate {@link Servlet} bean.
	 */
	public void setContextAttribute(String contextAttribute) {
		this.contextAttribute = contextAttribute;
	}

	/**
	 * Return the name of the ServletContext attribute which should be used to retrieve the
	 * {@link WebApplicationContext} from which to load the delegate {@link Servlet} bean.
	 */
	public String getContextAttribute() {
		return this.contextAttribute;
	}

	/**
	 * Set the name of the target bean in the Spring application context.
	 * The target bean must implement the standard Servlet Filter interface.
	 * <p>By default, the {@code servlet-name} as specified for the
	 * DelegatingServletProxy in {@code web.xml} will be used.
	 */
	public void setTargetBeanName(String targetBeanName) {
		this.targetBeanName = targetBeanName;
	}

	/**
	 * Return the name of the target bean in the Spring application context.
	 */
	protected String getTargetBeanName() {
		return this.targetBeanName;
	}

	/**
	 * Set whether to invoke the {@code Servlet.init} and
	 * {@code Servlet.destroy} lifecycle methods on the target bean.
	 * <p>Default is "false"; target beans usually rely on the Spring application
	 * context for managing their lifecycle. Setting this flag to "true" means
	 * that the servlet container will control the lifecycle of the target
	 * Servlet, with this proxy delegating the corresponding calls.
	 */
	public void setTargetServletLifecycle(boolean targetServletLifecycle) {
		this.targetServletLifecycle = targetServletLifecycle;
	}

	/**
	 * Return whether to invoke the {@code Servlet.init} and
	 * {@code Servlet.destroy} lifecycle methods on the target bean.
	 */
	protected boolean isTargetServletLifecycle() {
		return this.targetServletLifecycle;
	}

	@Override
	protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// Lazily initialize the delegate if necessary.
		Servlet delegateToUse = this.delegate;
		if (delegateToUse == null) {
			synchronized (this.delegateMonitor) {
				if (this.delegate == null) {
					WebApplicationContext wac = findWebApplicationContext();
					if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: "
							+ "no ContextLoaderListener registered?"); }
					this.delegate = initDelegate(wac);
				}
				delegateToUse = this.delegate;
			}
		}

		// Let the delegate perform the actual serice operation.
		invokeDelegate(delegateToUse, request, response);
	}

	/**
	 * Initialize the Servlet delegate, defined as bean the given Spring
	 * application context.
	 * <p>The default implementation fetches the bean from the application context
	 * and calls the standard {@code Servlet.init} method on it, passing
	 * in the ServletConfig of this Servlet proxy.
	 * @param wac the root application context
	 * @return the initialized delegate Servlet
	 * @throws ServletException if thrown by the Servlet
	 * @see #getTargetBeanName()
	 * @see #isTargetServletLifecycle()
	 * @see #getServletConfig()
	 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
	 */
	protected Servlet initDelegate(WebApplicationContext wac) throws ServletException {
		Servlet delegate = wac.getBean(getTargetBeanName(), Servlet.class);
		if (isTargetServletLifecycle()) {
			delegate.init(getServletConfig());
		}
		return delegate;
	}

	/**
	 * Actually invoke the delegate Servlet with the given request and response.
	 * @param delegate the delegate Servlet
	 * @param request the current HTTP request
	 * @param response the current HTTP response
	 * @throws ServletException if thrown by the Servlet
	 * @throws IOException if thrown by the Filter
	 */
	protected void invokeDelegate(Servlet delegate, ServletRequest request, ServletResponse response) throws ServletException,
			IOException {
		delegate.service(request, response);
	}
}
